using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

namespace FluentlyXml.UnitTests.Helpers
{
	/// <summary>
	/// A static unit testing helper class that can test two objects
	/// to see if they are equal.
	/// </summary>
	/// <remarks>
	/// This class does not rely on Object.Equals to insure that reference types
	/// are equal.  Instead, it compares all public properties of 
	/// reference types recursively (eventually arriving at value types).  If the properties are the same,
	/// then the objects are assuemd to be equal.  This may be changed
	/// to an optional behavior later on (if anyone has a need for that kind of thing).
	/// </remarks>
	/// <author>MBH</author>
	/// <dateAuthored>11/29/04</dateAuthored>
	public class EqualityTester
	{
		#region Private Staic Fields

		/// <summary>
		/// When set to true, DateTime objects may differ by &lt; 1 second and still
		/// be considered equal.
		/// </summary>
		private static bool mFuzzyDateMatching = false;

		#endregion

		#region Private Constructor

#if DEBUG

		/// <summary>
		/// This is a hack to insure 100% test coverage
		/// </summary>
		static EqualityTester()
		{
			new EqualityTester();
		}

#endif

		/// <summary>
		/// This is a static class, so it cannot be instantiated directly
		/// </summary>
		private EqualityTester()
		{
			return;
		}

		#endregion

		#region Private Static Methods

		/// <summary>
		/// Asserts that the result of enumerating two IEnumerable objects
		/// are identical.
		/// </summary>
		/// <param name="o1">The first IEnumerable object</param>
		/// <param name="o2">The second IEnumerable object</param>
		/// <param name="previousComparisons">The previous comparisons that have been made for this chain.</param>
		private static void AssertIEnumerablesEqual(object o1, object o2, Dictionary<Tuple<object, object>, bool> previousComparisons)
		{
			IEnumerator enumerator1 = ((IEnumerable)o1).GetEnumerator();
			IEnumerator enumerator2 = ((IEnumerable)o2).GetEnumerator();

			bool enumerator1Valid = enumerator1.MoveNext();
			bool enumerator2Valid = enumerator2.MoveNext();

			while (enumerator1Valid && enumerator2Valid)
			{
				//The two values from the enumerators
				object value1 = null;
				object value2 = null;

				value1 = enumerator1.Current;

				value2 = enumerator2.Current;

				AssertEqual(value1, value2, previousComparisons);

				enumerator1Valid = enumerator1.MoveNext();
				enumerator2Valid = enumerator2.MoveNext();
			}

			//If an enumerator is still valid, that means it had more items in it than the other
			if (enumerator1Valid)
				throw (new NotSupportedException("The first IEnumerable object had more items in it than the second."));

			if (enumerator2Valid)
				throw (new NotSupportedException("The second IEnumerable object had more items in it than the first."));
		}

		/// <summary>
		/// Asserts that two reference-types are equal by comparing their properties.
		/// </summary>
		/// <param name="o1">The first object.</param>
		/// <param name="o2">The second object.</param>
		/// <param name="previousComparisons">Contains all previously-executed comparisons.  Without this, recursive
		/// object graphs would cause stack overflow.</param>
		/// <exception cref="NotSupportedException"></exception>
		private static void AssertReferencesEqual(object o1, object o2, Dictionary<Tuple<object,object>,bool> previousComparisons)
		{
			//At this point, it really shouldn't be possible for two things to be of differing types, so no check is performed.

			//If these two objects have already been compared, we don't need to do it again.
			Tuple<object, object> comparison = new Tuple<object, object>(o1, o2);

			//If we have previously compared the two objects, there's no need to compare them again.
			//Doing so would lead to unbounded recursion.
			if (previousComparisons.ContainsKey(comparison))
			{
				return;
			}
			//Otherwise, log the comparison so we won't make it again in the future.
			else
			{
				previousComparisons.Add(comparison, true);
			}

			Type t = o1.GetType();

			//Strings are a special case
			if (t == typeof(string))
			{
				//If the strings aren't equal, fail
				if (!o1.Equals(o2))
				{
					throw (new NotSupportedException("Strings are not equal: '" + o1 + "' != '" + o2 + "'"));
				}
			}

			//Certain base-class library types should not be investigated due to strange behavior when you try
			//to crawl their object graphs.
			//This is a type used internally by .NET.  It leads to a huge object graph, so don't follow it...
			if (t.FullName == "System.RuntimeType")
			{
				return;
			}
			//Again, this is a huge object graph with some strange behaviors.
			else if (t == typeof(System.Globalization.CultureInfo))
			{
				return;
			}

			if (t.GetInterface("IEnumerable") != null || t.IsArray)
				AssertIEnumerablesEqual(o1, o2, previousComparisons);

			//Assert that all public properties are equal
			PropertyInfo[] properties = t.GetProperties();

			bool isICollection = t.GetInterface("ICollection") != null;

			foreach(PropertyInfo property in properties)
			{
				//If this is a collection, skip the SyncRoot property since
				//that can vary even if collections have identicial members
				if (isICollection && property.Name == "SyncRoot")
					continue;

				//Skip Capacity on ICollections, too, since that can change
				if (isICollection && property.Name == "Capacity")
					continue;

				MethodInfo getMethod = property.GetGetMethod();

				if (getMethod.GetParameters() != null && getMethod.GetParameters().Length > 0)
					//this property is an indexer and cannot be checked.  If the object implemeneted
					//IEnumerable, it was probably checked above.
					continue;

				if (getMethod != null)
				{
					//These are used to get the values from the properties.
					object value1, value2;

					try
					{
						value1 = getMethod.Invoke(o1, new object[] {});
					}
					catch (Exception ex)
					{
						value1 = ex;
					}

					try
					{
						value2 = getMethod.Invoke(o2, new object[] {});
					}
					catch (Exception ex)
					{
						value2 = ex;
					}

					//If both methods pitched an exception of the same type, allow it.
					if (value1 is Exception && value2 is Exception && value1.GetType() == value2.GetType())
					{
						continue;
					}

					//See if either result is null
					if (value1 == null || value2 == null)
					{
						//If either one is null, they both better be or the test failed.
						if (value1 != null || value2 != null)
							throw(new NotSupportedException("Object property accessor returned one null, one not-null for " + getMethod.Name));
						else
							continue;
					}

					if (value1.GetType().IsValueType)
					{
						try
						{
							AssertValuesEqual(value1, value2);
						}
						catch (NotSupportedException ex)
						{
							string errorMessage = String.Format("Property '{0}' failed equality check.  The inner exception is the reason.", property.Name);
							throw (new NotSupportedException(errorMessage, ex));
						}
					}
					//Don't recurse down the array graph, it's self referencing and will overflow the stack.
					else if (!(t.IsArray && value1.GetType().IsArray))
					{
						try
						{
							AssertReferencesEqual(value1, value2, previousComparisons);
						}
						catch (NotSupportedException ex)
						{
							string errorMessage = String.Format("Property '{0}' failed equality check.  The inner exception is the reason.", property.Name);
							throw (new NotSupportedException(errorMessage, ex));
						}
					}
				}
			}
		}

		/// <summary>
		/// Asserts that two value types are equal.
		/// </summary>
		/// <param name="o1">The first value.</param>
		/// <param name="o2">The second value</param>
		/// <exception cref="NotSupportedException">Thrown if the values aren't equal.</exception>
		private static void AssertValuesEqual(object o1, object o2)
		{
			if (o1.GetType() == typeof(DateTime))
			{
				AssertDateTimesEqual((DateTime)o1, (DateTime)o2);
			}
			else if (!o1.Equals(o2))
				throw(new NotSupportedException(String.Format("Values not equal: '{0}' != '{1}'", o1.ToString(), o2.ToString())));
		}

		/// <summary>
		/// Asserts that two DateTime objects are equal.
		/// </summary>
		/// <param name="dateTime1"></param>
		/// <param name="dateTime2"></param>
		private static void AssertDateTimesEqual(DateTime dateTime1, DateTime dateTime2)
		{
			if (mFuzzyDateMatching)
			{
				if (dateTime1 - dateTime2 > TimeSpan.FromSeconds(1) || dateTime1 - dateTime2 < TimeSpan.FromSeconds(-1))
					throw (new NotSupportedException(String.Format("DateTime's differ by more than 1 second with fuzzy matching enabled: '{0}' != '{1}'.", dateTime1, dateTime2)));
			}
			else if (dateTime1 != dateTime2)
				throw(new NotSupportedException(String.Format("Values not equal: '{0}' != '{1}'", dateTime1.ToString(), dateTime2.ToString())));
		}

		/// <summary>
		/// Asserts that all public properties on the specified objects
		/// are equal.
		/// </summary>
		/// <param name="o1">The first object</param>
		/// <param name="o2">The second object</param>
		/// <param name="previousComparisons">Contains all previously-made comaprisons.</param>
		/// <exception cref="InvalidOperationException">Thrown if the two objects are of different types, indicating an internal error.</exception>
		/// <exception cref="NotSupportedException">Thrown if the objects aren't equal.</exception>
		/// <remarks>
		/// o1 and o2 must be of the same type.  
		/// </remarks>
		private static void AssertEqual(object o1, object o2, Dictionary<Tuple<object, object>, bool> previousComparisons)
		{
			if (o1 == null || o2 == null)
			{
				//Both values better be null...
				if (o1 != null || o2 != null)
					throw (new NotSupportedException("One value type was null while the other was not."));
				else
					return;
			}

			if (o1.GetType() != o2.GetType())
				throw (new InvalidOperationException("AssertEqual called with objects of differing types: " + o1.GetType().Name + ", " + o2.GetType().Name));

			Type t = o1.GetType();

			if (t.IsValueType)
				AssertValuesEqual(o1, o2);
			else
				AssertReferencesEqual(o1, o2, previousComparisons);
		}

		#endregion

		#region Public Static Properties
		
		/// <summary>
		/// When set to true, DateTime objects may differ by &lt; 1 second and still
		/// be considered equal.
		/// </summary>
		public static bool FuzzyDateMatching
		{
			get
			{
				return mFuzzyDateMatching;
			}
			set
			{
				mFuzzyDateMatching = value;
			}
		}

		#endregion

		#region Public Static Methods

		/// <summary>
		/// Asserts that all public properties on the specified objects
		/// are equal.
		/// </summary>
		/// <param name="o1">The first object</param>
		/// <param name="o2">The second object</param>
		/// <exception cref="InvalidOperationException">Thrown if the two objects are of different types, indicating an internal error.</exception>
		/// <exception cref="NotSupportedException">Thrown if the objects aren't equal.</exception>
		/// <remarks>
		/// o1 and o2 must be of the same type.  
		/// </remarks>
		public static void AssertEqual(object o1, object o2)
		{
			Dictionary<Tuple<object, object>, bool> previousComparisons = new Dictionary<Tuple<object, object>, bool>();

			AssertEqual(o1, o2, previousComparisons);
		}

		/// <summary>
		/// Recursively tests that all public properties on the specified objects are equal.
		/// </summary>
		/// <param name="o1"></param>
		/// <param name="o2"></param>
		/// <exception cref="InvalidOperationException">Thrown if the two objects are not of the same type.</exception>
		/// <returns>true if the objects are equal, false otherwise.</returns>
		public static bool AreEqual(object o1, object o2)
		{
			try
			{
				AssertEqual(o1, o2);
			}
			catch (NotSupportedException)
			{
				return false;
			}

			return true;
		}

		#endregion
	}
}
